<template>
  <div :class="prefixCls">
    <div
      v-show="!isEdit"
      :class="{
        [`${prefixCls}__normal`]: true,
        'ellipsis-cell': column.ellipsis,
      }"
      @click="handleEdit"
    >
      <div class="cell-content" :title="column.ellipsis ? getValues ?? '' : ''">
        {{ getValues || getValues === 0 ? getValues : '&nbsp;' }}
      </div>
      <FormOutlined
        :class="`${prefixCls}__normal-icon`"
        v-if="!column.editRow"
      />
    </div>

    <a-spin v-if="isEdit" :spinning="spinning">
      <div :class="`${prefixCls}__wrapper`" v-click-outside="onClickOutside">
        <CellComponent
          v-bind="getComponentProps"
          :component="getComponent"
          :style="getWrapperStyle"
          :popoverVisible="getRuleVisible"
          :rule="getRule"
          :ruleMessage="ruleMessage"
          :class="getWrapperClass"
          ref="elRef"
          @change="handleChange"
          @options-change="handleOptionsChange"
          @pressEnter="handleEnter"
        />
        <div :class="`${prefixCls}__action`" v-if="!getRowEditable">
          <CheckOutlined
            :class="[`${prefixCls}__icon`, 'mx-2']"
            @click="handleSubmitClick"
          />
          <CloseOutlined :class="`${prefixCls}__icon `" @click="handleCancel" />
        </div>
      </div>
    </a-spin>
  </div>
</template>
<script lang="ts">
import type { CSSProperties, PropType } from 'vue'
import {
  computed,
  defineComponent,
  nextTick,
  ref,
  toRaw,
  unref,
  watchEffect,
} from 'vue'
import type { BasicColumn } from '../../types/table'
import type { EditRecordRow } from './index'
import {
  CheckOutlined,
  CloseOutlined,
  FormOutlined,
} from '@ant-design/icons-vue'
import { CellComponent } from './CellComponent'

import { useDesign } from '@/hooks/web/useDesign'
import { useTableContext } from '../../hooks/useTableContext'
import { clickOutside } from '@admin/directives'
import {
  isArray,
  isFunction,
  isString,
  isBoolean,
  isNumber,
  omit,
  pick,
  set,
} from '@admin/utils'
import { createPlaceholderMessage } from './helper'
import { treeToList } from '@admin/utils'
import { Spin } from 'ant-design-vue'

export default defineComponent({
  name: 'EditableCell',
  components: {
    FormOutlined,
    CloseOutlined,
    CheckOutlined,
    CellComponent,
    ASpin: Spin,
  },
  directives: {
    clickOutside,
  },
  props: {
    value: {
      type: [String, Number, Boolean, Object] as PropType<
        string | number | boolean | Recordable
      >,
      default: '',
    },
    record: {
      type: Object as PropType<EditRecordRow>,
    },
    column: {
      type: Object as PropType<BasicColumn>,
      default: () => ({}),
    },
    index: { type: Number },
  },
  setup(props) {
    const table = useTableContext()
    const isEdit = ref(false)
    const elRef = ref()
    const ruleVisible = ref(false)
    const ruleMessage = ref('')
    const optionsRef = ref<LabelValueOptions>([])
    const currentValueRef = ref<any>(props.value)
    const defaultValueRef = ref<any>(props.value)
    const spinning = ref<boolean>(false)

    const { prefixCls } = useDesign('editable-cell')

    const getComponent = computed(() => props.column?.editComponent || 'Input')
    const getRule = computed(() => props.column?.editRule)

    const getRuleVisible = computed(() => {
      return unref(ruleMessage) && unref(ruleVisible)
    })

    const getIsCheckComp = computed(() => {
      const component = unref(getComponent)
      return ['Checkbox', 'Switch'].includes(component)
    })

    const getComponentProps = computed(() => {
      const compProps = props.column?.editComponentProps ?? {}
      const component = unref(getComponent)
      const apiSelectProps: Recordable = {}
      if (component === 'ApiSelect') {
        apiSelectProps.cache = true
      }

      const isCheckValue = unref(getIsCheckComp)

      const valueField = isCheckValue ? 'checked' : 'value'
      const val = unref(currentValueRef)

      const value = isCheckValue
        ? isNumber(val) && isBoolean(val)
          ? val
          : !!val
        : val

      return {
        size: 'small',
        getPopupContainer: () => unref(table?.wrapRef.value) ?? document.body,
        getCalendarContainer: () =>
          unref(table?.wrapRef.value) ?? document.body,
        placeholder: createPlaceholderMessage(unref(getComponent)),
        ...apiSelectProps,
        ...omit(compProps, 'onChange'),
        [valueField]: value,
      }
    })

    const getValues = computed(() => {
      const { editComponentProps, editValueMap } = props.column

      const value = unref(currentValueRef)

      if (editValueMap && isFunction(editValueMap)) {
        return editValueMap(value)
      }

      const component = unref(getComponent)
      if (!component.includes('Select')) {
        return value
      }

      const options: LabelValueOptions =
        editComponentProps?.options ?? (unref(optionsRef) || [])
      const option = options.find((item) => `${item.value}` === `${value}`)

      return option?.label ?? value
    })

    const getWrapperStyle = computed((): CSSProperties => {
      if (unref(getIsCheckComp) || unref(getRowEditable)) {
        return {}
      }
      return {
        width: 'calc(100% - 48px)',
      }
    })

    const getWrapperClass = computed(() => {
      const { align = 'center' } = props.column
      return `edit-cell-align-${align}`
    })

    const getRowEditable = computed(() => {
      const { editable } = props.record || {}
      return !!editable
    })

    watchEffect(() => {
      defaultValueRef.value = props.value
      currentValueRef.value = props.value
    })

    watchEffect(() => {
      const { editable } = props.column
      if (isBoolean(editable) || isBoolean(unref(getRowEditable))) {
        isEdit.value = !!editable || unref(getRowEditable)
      }
    })

    function handleEdit() {
      if (unref(getRowEditable) || unref(props.column?.editRow)) return
      ruleMessage.value = ''
      isEdit.value = true
      nextTick(() => {
        const el = unref(elRef)
        el?.focus?.()
      })
    }

    async function handleChange(e: any) {
      const component = unref(getComponent)
      if (!e) {
        currentValueRef.value = e
      } else if (e?.target && Reflect.has(e.target, 'value')) {
        currentValueRef.value = (e as ChangeEvent).target.value
      } else if (component === 'Checkbox') {
        currentValueRef.value = (e as ChangeEvent).target.checked
      } else if (isString(e) || isBoolean(e) || isNumber(e)) {
        currentValueRef.value = e
      }
      const onChange = props.column?.editComponentProps?.onChange
      if (onChange && isFunction(onChange)) onChange(...arguments)

      table.emit?.('edit-change', {
        column: props.column,
        value: unref(currentValueRef),
        record: toRaw(props.record),
      })
      handleSubmiRule()
    }

    async function handleSubmiRule() {
      const { column, record } = props
      const { editRule } = column
      const currentValue = unref(currentValueRef)

      if (editRule) {
        if (isBoolean(editRule) && !currentValue && !isNumber(currentValue)) {
          ruleVisible.value = true
          const component = unref(getComponent)
          ruleMessage.value = createPlaceholderMessage(component)
          return false
        }
        if (isFunction(editRule)) {
          const res = await editRule(currentValue, record as Recordable)
          if (!!res) {
            ruleMessage.value = res
            ruleVisible.value = true
            return false
          } else {
            ruleMessage.value = ''
            return true
          }
        }
      }
      ruleMessage.value = ''
      return true
    }

    async function handleSubmit(needEmit = true, valid = true) {
      if (valid) {
        const isPass = await handleSubmiRule()
        if (!isPass) return false
      }

      const { column, index, record } = props
      if (!record) return false
      const { key, dataIndex } = column
      const value = unref(currentValueRef)
      if (!key && !dataIndex) return

      const dataKey = (dataIndex || key) as string

      if (!record.editable) {
        const { getBindValues } = table

        const { beforeEditSubmit, columns } = unref(getBindValues)

        if (beforeEditSubmit && isFunction(beforeEditSubmit)) {
          spinning.value = true
          const keys: string[] = columns
            .map((_column) => _column.dataIndex)
            .filter((field) => !!field) as string[]
          let result: any = true
          try {
            result = await beforeEditSubmit({
              record: pick(record, keys),
              index,
              key: key as string,
              value,
            })
          } catch (e) {
            result = false
          } finally {
            spinning.value = false
          }
          if (result === false) {
            return
          }
        }
      }

      set(record, dataKey, value)
      //const record = await table.updateTableData(index, dataKey, value);
      needEmit && table.emit?.('edit-end', { record, index, key, value })
      isEdit.value = false
    }

    async function handleEnter() {
      if (props.column?.editRow) {
        return
      }
      handleSubmit()
    }

    function handleSubmitClick() {
      handleSubmit()
    }

    function handleCancel() {
      isEdit.value = false
      currentValueRef.value = defaultValueRef.value
      const { column, index, record } = props
      const { key, dataIndex } = column
      table.emit?.('edit-cancel', {
        record,
        index,
        key: dataIndex || key,
        value: unref(currentValueRef),
      })
    }

    function onClickOutside() {
      if (props.column?.editable || unref(getRowEditable)) {
        return
      }
      const component = unref(getComponent)

      if (component.includes('Input')) {
        handleCancel()
      }
    }

    // only ApiSelect or TreeSelect
    function handleOptionsChange(options: LabelValueOptions) {
      const { replaceFields } = props.column?.editComponentProps ?? {}
      const component = unref(getComponent)
      if (component === 'ApiTreeSelect') {
        const {
          title = 'title',
          value = 'value',
          children = 'children',
        } = replaceFields || {}
        let listOptions: Recordable[] = treeToList(options, { children })
        listOptions = listOptions.map((item) => {
          return {
            label: item[title],
            value: item[value],
          }
        })
        optionsRef.value = listOptions as LabelValueOptions
      } else {
        optionsRef.value = options
      }
    }

    function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) {
      if (props.record) {
        /* eslint-disable  */
        isArray(props.record[cbs])
          ? props.record[cbs]?.push(handle)
          : (props.record[cbs] = [handle])
      }
    }

    if (props.record) {
      initCbs('submitCbs', handleSubmit)
      initCbs('validCbs', handleSubmiRule)
      initCbs('cancelCbs', handleCancel)

      if (props.column.dataIndex) {
        if (!props.record.editValueRefs) props.record.editValueRefs = {}
        props.record.editValueRefs[props.column.dataIndex] = currentValueRef
      }
      /* eslint-disable  */
      props.record.onCancelEdit = () => {
        isArray(props.record?.cancelCbs) &&
          props.record?.cancelCbs.forEach((fn) => fn())
      }
      /* eslint-disable */
      props.record.onSubmitEdit = async () => {
        if (isArray(props.record?.submitCbs)) {
          if (!props.record?.onValid?.()) return
          const submitFns = props.record?.submitCbs || []
          submitFns.forEach((fn) => fn(false, false))
          table.emit?.('edit-row-end')
          return true
        }
      }
    }

    return {
      isEdit,
      prefixCls,
      handleEdit,
      currentValueRef,
      handleSubmit,
      handleChange,
      handleCancel,
      elRef,
      getComponent,
      getRule,
      onClickOutside,
      ruleMessage,
      getRuleVisible,
      getComponentProps,
      handleOptionsChange,
      getWrapperStyle,
      getWrapperClass,
      getRowEditable,
      getValues,
      handleEnter,
      handleSubmitClick,
      spinning,
    }
  },
})
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-editable-cell';

.edit-cell-align-left {
  text-align: left;

  input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
    text-align: left;
  }
}

.edit-cell-align-center {
  text-align: center;

  input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
    text-align: center;
  }
}

.edit-cell-align-right {
  text-align: right;

  input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
    text-align: right;
  }
}

.edit-cell-rule-popover {
  .ant-popover-inner-content {
    padding: 4px 8px;
    color: @error-color;
    // border: 1px solid @error-color;
    border-radius: 2px;
  }
}
.@{prefix-cls} {
  position: relative;

  &__wrapper {
    display: flex;
    align-items: center;
    justify-content: center;

    > .ant-select {
      min-width: calc(100% - 50px);
    }
  }

  &__icon {
    &:hover {
      transform: scale(1.2);

      svg {
        color: @primary-color;
      }
    }
  }

  .ellipsis-cell {
    .cell-content {
      overflow-wrap: break-word;
      word-break: break-word;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }
  }

  &__normal {
    &-icon {
      position: absolute;
      top: 4px;
      right: 0;
      display: none;
      width: 20px;
      cursor: pointer;
    }
  }

  &:hover {
    .@{prefix-cls}__normal-icon {
      display: inline-block;
    }
  }
}
</style>
